/*
 *
 *  Copyright (C) 2019  Kui.Wang  <kui.wang@molchip.com>
 *
 * This file is subject to the terms and conditions of the GNU General Public
 * License.  See the file "COPYING" in the main directory of this archive
 * for more details.
 */
#include <linux/kernel.h>
#include <linux/debugfs.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/profile.h>
#include <linux/timex.h>
#include <linux/sched.h>
#include <linux/clockchips.h>
#include <linux/platform_device.h>
#include <linux/smp.h>
#include <linux/reboot.h>
#include <linux/uaccess.h>
#include <asm/system_misc.h>
#include <linux/regmap.h>
#include <linux/mfd/syscon.h>
#include <linux/io.h>
#include <linux/hrtimer.h>
#include <mach/io.h>
#include <mach/pmu.h>
#include <mach/fh_predefined.h>

#define PERF_IOCTL_MAGIC		'p'
#define SET_INTERVAL			_IOWR(PERF_IOCTL_MAGIC, 1, int)


#define PERF_DATA_BUF_LEN	(perfmon->ch_num * 8 * sizeof(int))

static volatile unsigned int clr_timer_cnt;

struct perfmon_info_t {
	unsigned int ctrl;
	unsigned int reserved[9];
	unsigned int rtrans_cnt;
	unsigned int rbw_cnt;
	unsigned int rlatency_cnt;
	unsigned int wtrans_cnt;
	unsigned int wbw_cnt;
	unsigned int wlatency_cnt;
	unsigned int bw_current_max;
	unsigned int rbw_cnt_hig;
	unsigned int wbw_cnt_hig;
};
static struct mc_perfmon *perfmon;

struct mc_perfmon {
	void __iomem		**regs;
	struct device		*dev;
	unsigned int		ch_num;
	struct perfmon_info_t   **perf_infos;
	int *data_buf;
	wait_queue_head_t read_wq;
	struct hrtimer timer;
	int read_valid;
	u64 read_interval_ms;
	u64 read_time_ns;
};

static ssize_t show_reg(struct device *dev, struct device_attribute *attr, char *buf)
{
	unsigned int i;
	unsigned int len = 0;

	fh_pmu_perfmon_rd_trigger();
	for(i=0;i<perfmon->ch_num;i++) {
		len += sprintf(buf+len, "CH[%d] RL : 0x%08x\n",i,perfmon->perf_infos[i]->rbw_cnt);
		len += sprintf(buf+len, "CH[%d] RH : 0x%08x\n",i,perfmon->perf_infos[i]->rbw_cnt_hig);
		len += sprintf(buf+len, "CH[%d] WL : 0x%08x\n",i,perfmon->perf_infos[i]->wbw_cnt);
		len += sprintf(buf+len, "CH[%d] WH : 0x%08x\n",i,perfmon->perf_infos[i]->wbw_cnt_hig);
		len += sprintf(buf+len, "CH[%d] RD : 0x%08x\n",i,perfmon->perf_infos[i]->rlatency_cnt);
		len += sprintf(buf+len, "CH[%d] WD : 0x%08x\n",i,perfmon->perf_infos[i]->wlatency_cnt);
	}

	return len;
}

static enum hrtimer_restart timer_cb(struct hrtimer *timer)
{
	fh_pmu_perfmon_rd_trigger();
	perfmon->read_time_ns = ktime_get_ns();
	perfmon->read_valid = 1;
	wake_up_interruptible(&perfmon->read_wq);

	return HRTIMER_NORESTART;
}


static DEVICE_ATTR(perfmon, S_IWUSR|S_IRUSR, show_reg, NULL);

static void *dmc_apb_base;
static int mc_perfmon_probe(struct platform_device *pdev)
{
	static struct kobject *mc_perf_kobj;
	struct resource *res;
	unsigned int i, ret, channel_num;

#ifdef CONFIG_USE_OF
	struct device_node *node = pdev->dev.of_node;

	ret = of_property_read_u32(node, "perfmon,channel-num",
							&channel_num);
	if (WARN_ON(ret)){
		pr_err("failed to map perfmon,channel-num \n");
		return -1;
	}
#else
	channel_num = (int)dev_get_platdata(&pdev->dev);
#endif

	dmc_apb_base = devm_ioremap(&pdev->dev, DMC_SYS_APB_REG_BASE, SZ_4K);
	if (!dmc_apb_base) {
		pr_err("failed to map dmc_apb_base \n");
		return -1;
	}

	fh_pmu_perfmon_init(0x1, 0xFFFFFFFF);

	perfmon = devm_kzalloc(&pdev->dev, sizeof(struct mc_perfmon), GFP_KERNEL);
	if (!perfmon)
		return -ENOMEM;

	perfmon->ch_num = channel_num;

	perfmon->regs = (void **)devm_kzalloc(&pdev->dev, channel_num * sizeof(void *), GFP_KERNEL);
	if (!perfmon->regs)
		return -ENOMEM;
	perfmon->data_buf = (int *)devm_kzalloc(&pdev->dev,
		PERF_DATA_BUF_LEN, GFP_KERNEL);
	if (!perfmon->data_buf)
		return -ENOMEM;
	perfmon->read_interval_ms = 1;
	hrtimer_init(&perfmon->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
	perfmon->timer.function = timer_cb;

	perfmon->perf_infos = (struct perfmon_info_t **)devm_kzalloc(&pdev->dev, channel_num * sizeof(struct perfmon_info_t *), GFP_KERNEL);
	if (!perfmon->perf_infos)
		return -ENOMEM;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

	for(i=0;i<perfmon->ch_num;i++) {
		perfmon->regs[i] = ioremap(res->start + (i << 20),res->end - res->start);
		if(IS_ERR(perfmon->regs[i]))
			return PTR_ERR(perfmon->regs[i]);
		perfmon->perf_infos[i] = (struct perfmon_info_t*)perfmon->regs[i];

		SET_REG(&perfmon->perf_infos[i]->ctrl, (0x1FF) | (0x1 << 10)); /* busmon en */
	}

	mc_perf_kobj = kobject_create_and_add("perf", NULL);
	ret = sysfs_create_file(mc_perf_kobj, &dev_attr_perfmon.attr);
	if (ret)
		printk(KERN_EMERG "Can not create the mc perfmon sysfs\n");
	dev_info(&pdev->dev, "FH perfmon Driver\n");
	return 0;
}

#ifdef CONFIG_USE_OF
static struct of_device_id mc_perfmon_of_match[] = {
        { .compatible = "fh-mc,perfmon", },
        { /* end of table */ }
};
MODULE_DEVICE_TABLE(of, mc_perfmon_of_match);
#endif

/* Driver Structure */
static struct platform_driver mc_perfmon_driver = {
	.probe          = mc_perfmon_probe,
	.driver         = {
		.name   = "fh-mc-perfmon",
#ifdef CONFIG_USE_OF
		.of_match_table = mc_perfmon_of_match,
#endif
	},
};

static u64 read_perf_data(void)
{
	unsigned int i;
	u64 time;
	int *cnts = perfmon->data_buf;

	if(clr_timer_cnt == 0) {
		fh_pmu_perfmon_timer_cnt_clr();
		fh_pmu_perfmon_rd_trigger();
		time = ktime_get_ns();
		init_waitqueue_head(&perfmon->read_wq);
		perfmon->read_valid = 0;
		hrtimer_start(&perfmon->timer,
			ms_to_ktime(perfmon->read_interval_ms), HRTIMER_MODE_REL);
		clr_timer_cnt = 1;
	} else {
		wait_event_interruptible(perfmon->read_wq, perfmon->read_valid);
		time = perfmon->read_time_ns;
		clr_timer_cnt = 0;
	}


	for(i=0;i<perfmon->ch_num;i++) {
		*(cnts++) = perfmon->perf_infos[i]->rtrans_cnt;
		*(cnts++) = perfmon->perf_infos[i]->rbw_cnt;
		*(cnts++) = perfmon->perf_infos[i]->rlatency_cnt;
		*(cnts++) = perfmon->perf_infos[i]->wtrans_cnt;
		*(cnts++) = perfmon->perf_infos[i]->wbw_cnt;
		*(cnts++) = perfmon->perf_infos[i]->wlatency_cnt;
		*(cnts++) = perfmon->perf_infos[i]->rbw_cnt_hig;
		*(cnts++) = perfmon->perf_infos[i]->wbw_cnt_hig;
	}

	return time;
}

static ssize_t perf_read(struct file *file, char __user *buf,
                        size_t count, loff_t *ppos)
{
	u64 time = read_perf_data();
	if (copy_to_user(buf, perfmon->data_buf, PERF_DATA_BUF_LEN)) {
			return -EFAULT;
	}
	if (copy_to_user(buf + PERF_DATA_BUF_LEN, &time, sizeof(time))) {
			return -EFAULT;
	}
	return count;
}

static long perf_ioctl(struct file *filp, unsigned int cmd,
	unsigned long arg)
{
	int t;

	switch (cmd) {
	case SET_INTERVAL:
		if (get_user(t, (int *)arg))
			return -EFAULT;
		perfmon->read_interval_ms = t;
		break;
	default:
		return -ENOTTY;
	}
	return 0;
}


static int perf_open(struct inode *inode, struct file *file)
{
	return 0;
}

static const struct file_operations perf_fops = {
	.owner          = THIS_MODULE,
	.read           = perf_read,
	.unlocked_ioctl = perf_ioctl,
	.open           = perf_open,
};

static struct miscdevice perf_dev = {
        .minor          = MISC_DYNAMIC_MINOR,
        .name           = "perf",
        .fops           = &perf_fops,
};

static int __init perf_init(void)
{
	misc_register(&perf_dev);
	return 0;
}

module_init(perf_init);
module_platform_driver(mc_perfmon_driver);
